[{"data":1,"prerenderedAt":1875},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fsync-google-sheets-with-excel-using-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fsync-google-sheets-with-excel-using-python":1866},{"id":4,"title":5,"body":6,"dateModified":1843,"datePublished":1843,"description":1844,"extension":1845,"faq":1846,"meta":1857,"navigation":235,"path":1858,"seo":1859,"slug":1862,"stem":1863,"type":1864,"__hash__":1865},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fsync-google-sheets-with-excel-using-python\u002Findex.md","Sync Google Sheets with Excel Using Python",{"type":7,"value":8,"toc":1827},"minimark",[9,28,156,161,197,200,204,207,355,362,366,458,464,553,567,571,728,731,735,738,954,971,1044,1048,1051,1303,1306,1310,1313,1519,1526,1530,1533,1568,1580,1584,1587,1657,1660,1664,1714,1718,1726,1730,1742,1746,1752,1761,1767,1779,1785,1789,1823],[10,11,12,13,17,18,21,22,27],"p",{},"Google Sheets and Excel end up on both sides of the same reporting process constantly: a team maintains a sheet of manual adjustments, a system produces ",[14,15,16],"code",{},".xlsx"," extracts, and something has to reconcile them. Python does the trip in either direction — ",[14,19,20],{},"gspread"," for the API and pandas for the reshaping — and once a service account is in place the sync runs unattended. This guide sets up authentication, moves data both ways, exports a whole spreadsheet as a workbook, and schedules the result. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002F","Moving Data Between Excel and Databases",".",[29,30,38,39,38,43,38,47,38,54,38,61,38,69,38,75,38,80,38,88,38,93,38,99,38,102,38,106,38,110,38,114,38,119,38,123,38,127,38,130,38,133,38,135,38,138,38,140,38,144,38,149,38,153],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"0 0 760 230","img",[34,35],"gs-t","gs-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"Both directions of a Sheets-to-Excel sync",[44,45,46],"desc",{"id":35},"A worksheet's values are read into a DataFrame and written to a formatted workbook, and a computed report is pushed back to a tab that colleagues can read in the browser.",[48,49],"rect",{"x":50,"y":50,"width":51,"height":52,"fill":53},"0","760","230","#ffffff",[55,56,60],"text",{"x":57,"y":58,"style":59},"380","26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","One credential, two directions",[48,62],{"x":63,"y":64,"width":65,"height":64,"rx":66,"fill":67,"stroke":68},"30","76","170","12","#d9f4f1","var(--line,#cdd5e6)",[55,70,74],{"x":71,"y":72,"style":73},"115","106","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Google Sheet",[55,76,79],{"x":71,"y":77,"style":78},"130","font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","edited by people",[81,82],"line",{"x1":83,"y1":84,"x2":85,"y2":84,"stroke":86,"style":87},"200","98","292","var(--brand,#5b5cf0)","stroke-width:2px",[89,90],"polygon",{"points":91,"fill":92},"292,98 282,93 282,103","#5b5cf0",[55,94,98],{"x":95,"y":96,"style":97},"246","88","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","read",[81,100],{"x1":85,"y1":77,"x2":83,"y2":77,"stroke":101,"style":87},"var(--accent,#f43f8f)",[89,103],{"points":104,"fill":105},"200,130 210,125 210,135","#be185d",[55,107,109],{"x":95,"y":108,"style":97},"150","write",[48,111],{"x":112,"y":64,"width":65,"height":64,"rx":66,"fill":113,"stroke":68},"296","#ebebfd",[55,115,118],{"x":116,"y":72,"style":117},"381","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas",[55,120,122],{"x":116,"y":77,"style":121},"font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","clean, join, aggregate",[81,124],{"x1":125,"y1":84,"x2":126,"y2":84,"stroke":86,"style":87},"466","558",[89,128],{"points":129,"fill":92},"558,98 548,93 548,103",[55,131,109],{"x":132,"y":96,"style":97},"512",[81,134],{"x1":126,"y1":77,"x2":125,"y2":77,"stroke":101,"style":87},[89,136],{"points":137,"fill":105},"466,130 476,125 476,135",[55,139,98],{"x":132,"y":108,"style":97},[48,141],{"x":142,"y":64,"width":65,"height":64,"rx":66,"fill":143,"stroke":68},"562","#fdefd8",[55,145,148],{"x":146,"y":72,"style":147},"647","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",".xlsx report",[55,150,152],{"x":146,"y":77,"style":151},"font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","formatted, delivered",[55,154,155],{"x":57,"y":83,"style":121},"Values move; presentation is rebuilt at each end",[157,158,160],"h2",{"id":159},"prerequisites","Prerequisites",[162,163,168],"pre",{"className":164,"code":165,"language":166,"meta":167,"style":167},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install gspread google-auth pandas openpyxl xlsxwriter\n","bash","",[14,169,170],{"__ignoreMap":167},[171,172,174,178,182,185,188,191,194],"span",{"class":81,"line":173},1,[171,175,177],{"class":176},"sMTad","pip",[171,179,181],{"class":180},"srMev"," install",[171,183,184],{"class":180}," gspread",[171,186,187],{"class":180}," google-auth",[171,189,190],{"class":180}," pandas",[171,192,193],{"class":180}," openpyxl",[171,195,196],{"class":180}," xlsxwriter\n",[10,198,199],{},"You also need a Google Cloud project with the Sheets and Drive APIs enabled, and a service-account key in JSON form.",[157,201,203],{"id":202},"authenticate-as-a-service-account","Authenticate as a service account",[10,205,206],{},"A service account is its own identity with its own email address. Create the key, store it outside the repository, and point the code at it through an environment variable:",[162,208,212],{"className":209,"code":210,"language":211,"meta":167,"style":167},"language-python shiki shiki-themes github-light github-dark-high-contrast","\"\"\"Authorise gspread with a service account.\"\"\"\nimport os\n\nimport gspread\nfrom google.oauth2.service_account import Credentials\n\nSCOPES = [\n    \"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fspreadsheets\",\n    \"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive.readonly\",\n]\n\ncreds = Credentials.from_service_account_file(\n    os.environ[\"GOOGLE_APPLICATION_CREDENTIALS\"], scopes=SCOPES\n)\nclient = gspread.authorize(creds)\n","python",[14,213,214,219,230,237,245,259,264,277,286,294,300,305,317,338,344],{"__ignoreMap":167},[171,215,216],{"class":81,"line":173},[171,217,218],{"class":180},"\"\"\"Authorise gspread with a service account.\"\"\"\n",[171,220,222,226],{"class":81,"line":221},2,[171,223,225],{"class":224},"s-kum","import",[171,227,229],{"class":228},"skGVy"," os\n",[171,231,233],{"class":81,"line":232},3,[171,234,236],{"emptyLinePlaceholder":235},true,"\n",[171,238,240,242],{"class":81,"line":239},4,[171,241,225],{"class":224},[171,243,244],{"class":228}," gspread\n",[171,246,248,251,254,256],{"class":81,"line":247},5,[171,249,250],{"class":224},"from",[171,252,253],{"class":228}," google.oauth2.service_account ",[171,255,225],{"class":224},[171,257,258],{"class":228}," Credentials\n",[171,260,262],{"class":81,"line":261},6,[171,263,236],{"emptyLinePlaceholder":235},[171,265,267,271,274],{"class":81,"line":266},7,[171,268,270],{"class":269},"sP0c6","SCOPES",[171,272,273],{"class":224}," =",[171,275,276],{"class":228}," [\n",[171,278,280,283],{"class":81,"line":279},8,[171,281,282],{"class":180},"    \"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fspreadsheets\"",[171,284,285],{"class":228},",\n",[171,287,289,292],{"class":81,"line":288},9,[171,290,291],{"class":180},"    \"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive.readonly\"",[171,293,285],{"class":228},[171,295,297],{"class":81,"line":296},10,[171,298,299],{"class":228},"]\n",[171,301,303],{"class":81,"line":302},11,[171,304,236],{"emptyLinePlaceholder":235},[171,306,308,311,314],{"class":81,"line":307},12,[171,309,310],{"class":228},"creds ",[171,312,313],{"class":224},"=",[171,315,316],{"class":228}," Credentials.from_service_account_file(\n",[171,318,320,323,326,329,333,335],{"class":81,"line":319},13,[171,321,322],{"class":228},"    os.environ[",[171,324,325],{"class":180},"\"GOOGLE_APPLICATION_CREDENTIALS\"",[171,327,328],{"class":228},"], ",[171,330,332],{"class":331},"sa561","scopes",[171,334,313],{"class":224},[171,336,337],{"class":269},"SCOPES\n",[171,339,341],{"class":81,"line":340},14,[171,342,343],{"class":228},")\n",[171,345,347,350,352],{"class":81,"line":346},15,[171,348,349],{"class":228},"client ",[171,351,313],{"class":224},[171,353,354],{"class":228}," gspread.authorize(creds)\n",[10,356,357,358,361],{},"The step people miss is sharing: open the spreadsheet in the browser and share it with the ",[14,359,360],{},"client_email"," from the JSON key, exactly as you would with a colleague. Without that, every request returns a 403 on a document you can see perfectly well yourself.",[157,363,365],{"id":364},"read-a-tab-into-pandas","Read a tab into pandas",[162,367,369],{"className":209,"code":368,"language":211,"meta":167,"style":167},"import pandas as pd\n\nsheet = client.open_by_key(\"1AbCdEf...\")        # the id from the URL\ntab = sheet.worksheet(\"Adjustments\")\n\nrecords = tab.get_all_records()                 # first row becomes the keys\ndf = pd.DataFrame(records)\nprint(df.dtypes)\n",[14,370,371,384,388,408,423,427,440,450],{"__ignoreMap":167},[171,372,373,375,378,381],{"class":81,"line":173},[171,374,225],{"class":224},[171,376,377],{"class":228}," pandas ",[171,379,380],{"class":224},"as",[171,382,383],{"class":228}," pd\n",[171,385,386],{"class":81,"line":221},[171,387,236],{"emptyLinePlaceholder":235},[171,389,390,393,395,398,401,404],{"class":81,"line":232},[171,391,392],{"class":228},"sheet ",[171,394,313],{"class":224},[171,396,397],{"class":228}," client.open_by_key(",[171,399,400],{"class":180},"\"1AbCdEf...\"",[171,402,403],{"class":228},")        ",[171,405,407],{"class":406},"s-wDw","# the id from the URL\n",[171,409,410,413,415,418,421],{"class":81,"line":239},[171,411,412],{"class":228},"tab ",[171,414,313],{"class":224},[171,416,417],{"class":228}," sheet.worksheet(",[171,419,420],{"class":180},"\"Adjustments\"",[171,422,343],{"class":228},[171,424,425],{"class":81,"line":247},[171,426,236],{"emptyLinePlaceholder":235},[171,428,429,432,434,437],{"class":81,"line":261},[171,430,431],{"class":228},"records ",[171,433,313],{"class":224},[171,435,436],{"class":228}," tab.get_all_records()                 ",[171,438,439],{"class":406},"# first row becomes the keys\n",[171,441,442,445,447],{"class":81,"line":266},[171,443,444],{"class":228},"df ",[171,446,313],{"class":224},[171,448,449],{"class":228}," pd.DataFrame(records)\n",[171,451,452,455],{"class":81,"line":279},[171,453,454],{"class":269},"print",[171,456,457],{"class":228},"(df.dtypes)\n",[10,459,460,463],{},[14,461,462],{},"get_all_records"," is one API call for the whole tab, which matters for quota. Everything arrives as text or numbers according to what the sheet holds, so pin the types you care about immediately:",[162,465,467],{"className":209,"code":466,"language":211,"meta":167,"style":167},"df[\"adjustment\"] = pd.to_numeric(df[\"adjustment\"], errors=\"coerce\")\ndf[\"effective\"] = pd.to_datetime(df[\"effective\"], errors=\"coerce\")\ndf[\"reference\"] = df[\"reference\"].astype(\"string\")     # keep leading zeros\n",[14,468,469,499,525],{"__ignoreMap":167},[171,470,471,474,477,480,482,485,487,489,492,494,497],{"class":81,"line":173},[171,472,473],{"class":228},"df[",[171,475,476],{"class":180},"\"adjustment\"",[171,478,479],{"class":228},"] ",[171,481,313],{"class":224},[171,483,484],{"class":228}," pd.to_numeric(df[",[171,486,476],{"class":180},[171,488,328],{"class":228},[171,490,491],{"class":331},"errors",[171,493,313],{"class":224},[171,495,496],{"class":180},"\"coerce\"",[171,498,343],{"class":228},[171,500,501,503,506,508,510,513,515,517,519,521,523],{"class":81,"line":221},[171,502,473],{"class":228},[171,504,505],{"class":180},"\"effective\"",[171,507,479],{"class":228},[171,509,313],{"class":224},[171,511,512],{"class":228}," pd.to_datetime(df[",[171,514,505],{"class":180},[171,516,328],{"class":228},[171,518,491],{"class":331},[171,520,313],{"class":224},[171,522,496],{"class":180},[171,524,343],{"class":228},[171,526,527,529,532,534,536,539,541,544,547,550],{"class":81,"line":232},[171,528,473],{"class":228},[171,530,531],{"class":180},"\"reference\"",[171,533,479],{"class":228},[171,535,313],{"class":224},[171,537,538],{"class":228}," df[",[171,540,531],{"class":180},[171,542,543],{"class":228},"].astype(",[171,545,546],{"class":180},"\"string\"",[171,548,549],{"class":228},")     ",[171,551,552],{"class":406},"# keep leading zeros\n",[10,554,555,558,559,562,563,566],{},[14,556,557],{},"errors=\"coerce\""," turns the inevitable stray text into ",[14,560,561],{},"NaT"," or ",[14,564,565],{},"NaN"," rather than raising — then count them, because a handful of unparsed values is a data-quality signal worth reporting.",[157,568,570],{"id":569},"write-a-formatted-workbook-from-the-sheet","Write a formatted workbook from the sheet",[162,572,574],{"className":209,"code":573,"language":211,"meta":167,"style":167},"with pd.ExcelWriter(\"adjustments.xlsx\", engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, index=False, sheet_name=\"Adjustments\")\n    book, ws = writer.book, writer.sheets[\"Adjustments\"]\n    money = book.add_format({\"num_format\": \"#,##0.00\"})\n    ws.set_column(\"C:C\", 14, money)\n    ws.freeze_panes(1, 0)\n    ws.autofilter(0, 0, len(df), len(df.columns) - 1)\n",[14,575,576,606,630,644,666,682,696],{"__ignoreMap":167},[171,577,578,581,584,587,590,593,595,598,601,603],{"class":81,"line":173},[171,579,580],{"class":224},"with",[171,582,583],{"class":228}," pd.ExcelWriter(",[171,585,586],{"class":180},"\"adjustments.xlsx\"",[171,588,589],{"class":228},", ",[171,591,592],{"class":331},"engine",[171,594,313],{"class":224},[171,596,597],{"class":180},"\"xlsxwriter\"",[171,599,600],{"class":228},") ",[171,602,380],{"class":224},[171,604,605],{"class":228}," writer:\n",[171,607,608,611,614,616,619,621,624,626,628],{"class":81,"line":221},[171,609,610],{"class":228},"    df.to_excel(writer, ",[171,612,613],{"class":331},"index",[171,615,313],{"class":224},[171,617,618],{"class":269},"False",[171,620,589],{"class":228},[171,622,623],{"class":331},"sheet_name",[171,625,313],{"class":224},[171,627,420],{"class":180},[171,629,343],{"class":228},[171,631,632,635,637,640,642],{"class":81,"line":232},[171,633,634],{"class":228},"    book, ws ",[171,636,313],{"class":224},[171,638,639],{"class":228}," writer.book, writer.sheets[",[171,641,420],{"class":180},[171,643,299],{"class":228},[171,645,646,649,651,654,657,660,663],{"class":81,"line":239},[171,647,648],{"class":228},"    money ",[171,650,313],{"class":224},[171,652,653],{"class":228}," book.add_format({",[171,655,656],{"class":180},"\"num_format\"",[171,658,659],{"class":228},": ",[171,661,662],{"class":180},"\"#,##0.00\"",[171,664,665],{"class":228},"})\n",[171,667,668,671,674,676,679],{"class":81,"line":247},[171,669,670],{"class":228},"    ws.set_column(",[171,672,673],{"class":180},"\"C:C\"",[171,675,589],{"class":228},[171,677,678],{"class":269},"14",[171,680,681],{"class":228},", money)\n",[171,683,684,687,690,692,694],{"class":81,"line":261},[171,685,686],{"class":228},"    ws.freeze_panes(",[171,688,689],{"class":269},"1",[171,691,589],{"class":228},[171,693,50],{"class":269},[171,695,343],{"class":228},[171,697,698,701,703,705,707,709,712,715,717,720,723,726],{"class":81,"line":266},[171,699,700],{"class":228},"    ws.autofilter(",[171,702,50],{"class":269},[171,704,589],{"class":228},[171,706,50],{"class":269},[171,708,589],{"class":228},[171,710,711],{"class":269},"len",[171,713,714],{"class":228},"(df), ",[171,716,711],{"class":269},[171,718,719],{"class":228},"(df.columns) ",[171,721,722],{"class":224},"-",[171,724,725],{"class":269}," 1",[171,727,343],{"class":228},[10,729,730],{},"Formatting does not travel with the values, so it is rebuilt here — which is an advantage as often as it is a cost, since the workbook can follow your own report conventions rather than whatever the sheet's authors chose.",[157,732,734],{"id":733},"push-a-report-back-to-a-tab","Push a report back to a tab",[10,736,737],{},"The other direction replaces a tab's contents in one call:",[162,739,741],{"className":209,"code":740,"language":211,"meta":167,"style":167},"import gspread\n\nsummary = (df.groupby(\"region\", as_index=False)[\"adjustment\"].sum()\n             .sort_values(\"adjustment\", ascending=False))\n\ntry:\n    out = sheet.worksheet(\"Summary\")\n    out.clear()\nexcept gspread.WorksheetNotFound:\n    out = sheet.add_worksheet(title=\"Summary\", rows=200, cols=10)\n\nout.update(\n    [summary.columns.tolist()] + summary.astype(object).where(summary.notna(), \"\").values.tolist(),\n    value_input_option=\"USER_ENTERED\",\n)\nout.format(\"A1:B1\", {\"textFormat\": {\"bold\": True}})\n",[14,742,743,749,753,783,802,806,814,828,833,841,877,881,886,909,921,925],{"__ignoreMap":167},[171,744,745,747],{"class":81,"line":173},[171,746,225],{"class":224},[171,748,244],{"class":228},[171,750,751],{"class":81,"line":221},[171,752,236],{"emptyLinePlaceholder":235},[171,754,755,758,760,763,766,768,771,773,775,778,780],{"class":81,"line":232},[171,756,757],{"class":228},"summary ",[171,759,313],{"class":224},[171,761,762],{"class":228}," (df.groupby(",[171,764,765],{"class":180},"\"region\"",[171,767,589],{"class":228},[171,769,770],{"class":331},"as_index",[171,772,313],{"class":224},[171,774,618],{"class":269},[171,776,777],{"class":228},")[",[171,779,476],{"class":180},[171,781,782],{"class":228},"].sum()\n",[171,784,785,788,790,792,795,797,799],{"class":81,"line":239},[171,786,787],{"class":228},"             .sort_values(",[171,789,476],{"class":180},[171,791,589],{"class":228},[171,793,794],{"class":331},"ascending",[171,796,313],{"class":224},[171,798,618],{"class":269},[171,800,801],{"class":228},"))\n",[171,803,804],{"class":81,"line":247},[171,805,236],{"emptyLinePlaceholder":235},[171,807,808,811],{"class":81,"line":261},[171,809,810],{"class":224},"try",[171,812,813],{"class":228},":\n",[171,815,816,819,821,823,826],{"class":81,"line":266},[171,817,818],{"class":228},"    out ",[171,820,313],{"class":224},[171,822,417],{"class":228},[171,824,825],{"class":180},"\"Summary\"",[171,827,343],{"class":228},[171,829,830],{"class":81,"line":279},[171,831,832],{"class":228},"    out.clear()\n",[171,834,835,838],{"class":81,"line":288},[171,836,837],{"class":224},"except",[171,839,840],{"class":228}," gspread.WorksheetNotFound:\n",[171,842,843,845,847,850,852,854,856,858,861,863,865,867,870,872,875],{"class":81,"line":296},[171,844,818],{"class":228},[171,846,313],{"class":224},[171,848,849],{"class":228}," sheet.add_worksheet(",[171,851,40],{"class":331},[171,853,313],{"class":224},[171,855,825],{"class":180},[171,857,589],{"class":228},[171,859,860],{"class":331},"rows",[171,862,313],{"class":224},[171,864,83],{"class":269},[171,866,589],{"class":228},[171,868,869],{"class":331},"cols",[171,871,313],{"class":224},[171,873,874],{"class":269},"10",[171,876,343],{"class":228},[171,878,879],{"class":81,"line":302},[171,880,236],{"emptyLinePlaceholder":235},[171,882,883],{"class":81,"line":307},[171,884,885],{"class":228},"out.update(\n",[171,887,888,891,894,897,900,903,906],{"class":81,"line":319},[171,889,890],{"class":228},"    [summary.columns.tolist()] ",[171,892,893],{"class":224},"+",[171,895,896],{"class":228}," summary.astype(",[171,898,899],{"class":269},"object",[171,901,902],{"class":228},").where(summary.notna(), ",[171,904,905],{"class":180},"\"\"",[171,907,908],{"class":228},").values.tolist(),\n",[171,910,911,914,916,919],{"class":81,"line":340},[171,912,913],{"class":331},"    value_input_option",[171,915,313],{"class":224},[171,917,918],{"class":180},"\"USER_ENTERED\"",[171,920,285],{"class":228},[171,922,923],{"class":81,"line":346},[171,924,343],{"class":228},[171,926,928,931,934,937,940,943,946,948,951],{"class":81,"line":927},16,[171,929,930],{"class":228},"out.format(",[171,932,933],{"class":180},"\"A1:B1\"",[171,935,936],{"class":228},", {",[171,938,939],{"class":180},"\"textFormat\"",[171,941,942],{"class":228},": {",[171,944,945],{"class":180},"\"bold\"",[171,947,659],{"class":228},[171,949,950],{"class":269},"True",[171,952,953],{"class":228},"}})\n",[10,955,956,957,960,961,964,965,967,968,970],{},"Two details matter. ",[14,958,959],{},"value_input_option=\"USER_ENTERED\""," makes Sheets parse dates and numbers as a person typing them would; ",[14,962,963],{},"RAW"," stores everything as text. And ",[14,966,565],{}," must become ",[14,969,905],{}," before the update — the API rejects it, since it is not valid JSON.",[29,972,38,977,38,980,38,983,38,986,38,992,38,996,38,1000,38,1007,38,1012,38,1014,38,1018,38,1020,38,1025,38,1028,38,1033,38,1035,38,1038,38,1040],{"viewBox":973,"role":32,"ariaLabelledBy":974,"xmlns":36,"style":37},"0 0 760 220",[975,976],"gs2-t","gs2-d",[40,978,979],{"id":975},"Batching keeps a sync inside the API quota",[44,981,982],{"id":976},"Reading cell by cell and writing row by row costs one API call each and exhausts the per-minute quota, while one bulk read and one bulk update cost two calls in total.",[48,984],{"x":50,"y":50,"width":51,"height":985,"fill":53},"220",[55,987,991],{"x":988,"y":989,"style":990},"196","28","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","per-cell access",[55,993,995],{"x":994,"y":989,"style":73},"566","batched access",[81,997],{"x1":57,"y1":998,"x2":57,"y2":83,"stroke":68,"style":999},"40","stroke-width:1px",[48,1001],{"x":63,"y":1002,"width":1003,"height":1004,"rx":1005,"fill":1006,"stroke":68},"46","332","44","9","#fee8f2",[55,1008,1011],{"x":988,"y":1009,"style":1010},"74","font-size:12px;fill:var(--accent-ink,#be185d);text-anchor:middle","tab.cell(r, c) — one call per cell",[48,1013],{"x":63,"y":84,"width":1003,"height":1004,"rx":1005,"fill":1006,"stroke":68},[55,1015,1017],{"x":988,"y":1016,"style":1010},"126","500 rows = thousands of calls",[48,1019],{"x":63,"y":108,"width":1003,"height":1004,"rx":1005,"fill":1006,"stroke":68},[55,1021,1024],{"x":988,"y":1022,"style":1023},"178","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","429 rate-limit errors",[48,1026],{"x":1027,"y":1002,"width":1003,"height":1004,"rx":1005,"fill":67,"stroke":68},"398",[55,1029,1032],{"x":1030,"y":1009,"style":1031},"564","font-size:12px;fill:var(--teal-ink,#0b6157);text-anchor:middle","get_all_records() — one call",[48,1034],{"x":1027,"y":84,"width":1003,"height":1004,"rx":1005,"fill":67,"stroke":68},[55,1036,1037],{"x":1030,"y":1016,"style":1031},"update(values) — one call",[48,1039],{"x":1027,"y":108,"width":1003,"height":1004,"rx":1005,"fill":67,"stroke":68},[55,1041,1043],{"x":1030,"y":1022,"style":1042},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","comfortably inside quota",[157,1045,1047],{"id":1046},"export-the-whole-spreadsheet-as-a-workbook","Export the whole spreadsheet as a workbook",[10,1049,1050],{},"When you want every tab and no transformation, ask Drive to do the conversion:",[162,1052,1054],{"className":209,"code":1053,"language":211,"meta":167,"style":167},"\"\"\"Download a Google spreadsheet as a real .xlsx.\"\"\"\nimport os\n\nimport requests\nfrom google.auth.transport.requests import Request\nfrom google.oauth2.service_account import Credentials\n\ncreds = Credentials.from_service_account_file(\n    os.environ[\"GOOGLE_APPLICATION_CREDENTIALS\"],\n    scopes=[\"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive.readonly\"],\n)\ncreds.refresh(Request())\n\nfile_id = \"1AbCdEf...\"\nurl = f\"https:\u002F\u002Fwww.googleapis.com\u002Fdrive\u002Fv3\u002Ffiles\u002F{file_id}\u002Fexport\"\nresp = requests.get(url, params={\"mimeType\":\n    \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"},\n    headers={\"Authorization\": f\"Bearer {creds.token}\"}, timeout=60)\nresp.raise_for_status()\n\nwith open(\"spreadsheet.xlsx\", \"wb\") as fh:\n    fh.write(resp.content)\n",[14,1055,1056,1061,1067,1071,1078,1090,1100,1104,1112,1121,1136,1140,1145,1149,1159,1185,1207,1216,1260,1266,1271,1297],{"__ignoreMap":167},[171,1057,1058],{"class":81,"line":173},[171,1059,1060],{"class":180},"\"\"\"Download a Google spreadsheet as a real .xlsx.\"\"\"\n",[171,1062,1063,1065],{"class":81,"line":221},[171,1064,225],{"class":224},[171,1066,229],{"class":228},[171,1068,1069],{"class":81,"line":232},[171,1070,236],{"emptyLinePlaceholder":235},[171,1072,1073,1075],{"class":81,"line":239},[171,1074,225],{"class":224},[171,1076,1077],{"class":228}," requests\n",[171,1079,1080,1082,1085,1087],{"class":81,"line":247},[171,1081,250],{"class":224},[171,1083,1084],{"class":228}," google.auth.transport.requests ",[171,1086,225],{"class":224},[171,1088,1089],{"class":228}," Request\n",[171,1091,1092,1094,1096,1098],{"class":81,"line":261},[171,1093,250],{"class":224},[171,1095,253],{"class":228},[171,1097,225],{"class":224},[171,1099,258],{"class":228},[171,1101,1102],{"class":81,"line":266},[171,1103,236],{"emptyLinePlaceholder":235},[171,1105,1106,1108,1110],{"class":81,"line":279},[171,1107,310],{"class":228},[171,1109,313],{"class":224},[171,1111,316],{"class":228},[171,1113,1114,1116,1118],{"class":81,"line":288},[171,1115,322],{"class":228},[171,1117,325],{"class":180},[171,1119,1120],{"class":228},"],\n",[171,1122,1123,1126,1128,1131,1134],{"class":81,"line":296},[171,1124,1125],{"class":331},"    scopes",[171,1127,313],{"class":224},[171,1129,1130],{"class":228},"[",[171,1132,1133],{"class":180},"\"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive.readonly\"",[171,1135,1120],{"class":228},[171,1137,1138],{"class":81,"line":302},[171,1139,343],{"class":228},[171,1141,1142],{"class":81,"line":307},[171,1143,1144],{"class":228},"creds.refresh(Request())\n",[171,1146,1147],{"class":81,"line":319},[171,1148,236],{"emptyLinePlaceholder":235},[171,1150,1151,1154,1156],{"class":81,"line":340},[171,1152,1153],{"class":228},"file_id ",[171,1155,313],{"class":224},[171,1157,1158],{"class":180}," \"1AbCdEf...\"\n",[171,1160,1161,1164,1166,1169,1172,1176,1179,1182],{"class":81,"line":346},[171,1162,1163],{"class":228},"url ",[171,1165,313],{"class":224},[171,1167,1168],{"class":224}," f",[171,1170,1171],{"class":180},"\"https:\u002F\u002Fwww.googleapis.com\u002Fdrive\u002Fv3\u002Ffiles\u002F",[171,1173,1175],{"class":1174},"sSjpA","{",[171,1177,1178],{"class":228},"file_id",[171,1180,1181],{"class":1174},"}",[171,1183,1184],{"class":180},"\u002Fexport\"\n",[171,1186,1187,1190,1192,1195,1198,1200,1202,1205],{"class":81,"line":927},[171,1188,1189],{"class":228},"resp ",[171,1191,313],{"class":224},[171,1193,1194],{"class":228}," requests.get(url, ",[171,1196,1197],{"class":331},"params",[171,1199,313],{"class":224},[171,1201,1175],{"class":228},[171,1203,1204],{"class":180},"\"mimeType\"",[171,1206,813],{"class":228},[171,1208,1210,1213],{"class":81,"line":1209},17,[171,1211,1212],{"class":180},"    \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"",[171,1214,1215],{"class":228},"},\n",[171,1217,1219,1222,1224,1226,1229,1231,1234,1237,1239,1242,1244,1247,1250,1253,1255,1258],{"class":81,"line":1218},18,[171,1220,1221],{"class":331},"    headers",[171,1223,313],{"class":224},[171,1225,1175],{"class":228},[171,1227,1228],{"class":180},"\"Authorization\"",[171,1230,659],{"class":228},[171,1232,1233],{"class":224},"f",[171,1235,1236],{"class":180},"\"Bearer ",[171,1238,1175],{"class":1174},[171,1240,1241],{"class":228},"creds.token",[171,1243,1181],{"class":1174},[171,1245,1246],{"class":180},"\"",[171,1248,1249],{"class":228},"}, ",[171,1251,1252],{"class":331},"timeout",[171,1254,313],{"class":224},[171,1256,1257],{"class":269},"60",[171,1259,343],{"class":228},[171,1261,1263],{"class":81,"line":1262},19,[171,1264,1265],{"class":228},"resp.raise_for_status()\n",[171,1267,1269],{"class":81,"line":1268},20,[171,1270,236],{"emptyLinePlaceholder":235},[171,1272,1274,1276,1279,1282,1285,1287,1290,1292,1294],{"class":81,"line":1273},21,[171,1275,580],{"class":224},[171,1277,1278],{"class":269}," open",[171,1280,1281],{"class":228},"(",[171,1283,1284],{"class":180},"\"spreadsheet.xlsx\"",[171,1286,589],{"class":228},[171,1288,1289],{"class":180},"\"wb\"",[171,1291,600],{"class":228},[171,1293,380],{"class":224},[171,1295,1296],{"class":228}," fh:\n",[171,1298,1300],{"class":81,"line":1299},22,[171,1301,1302],{"class":228},"    fh.write(resp.content)\n",[10,1304,1305],{},"This preserves tabs, values and much of the formatting, and it is a single request. Read it back with any normal reader afterwards.",[157,1307,1309],{"id":1308},"reconcile-the-two-sides","Reconcile the two sides",[10,1311,1312],{},"The point of a sync is usually a comparison — which manual adjustments have no matching order, and which orders were adjusted twice:",[162,1314,1316],{"className":209,"code":1315,"language":211,"meta":167,"style":167},"import pandas as pd\n\norders = pd.read_excel(\"orders.xlsx\", dtype={\"reference\": \"string\"})\nadjustments = df                                   # from the sheet, above\n\nmerged = orders.merge(adjustments, on=\"reference\", how=\"left\", indicator=True)\nunmatched = adjustments[~adjustments[\"reference\"].isin(orders[\"reference\"])]\nduplicated = adjustments[adjustments.duplicated(\"reference\", keep=False)]\n\nprint(f\"{len(unmatched)} adjustment(s) match no order\")\nprint(f\"{len(duplicated)} reference(s) adjusted more than once\")\n",[14,1317,1318,1328,1332,1362,1375,1379,1417,1443,1467,1471,1495],{"__ignoreMap":167},[171,1319,1320,1322,1324,1326],{"class":81,"line":173},[171,1321,225],{"class":224},[171,1323,377],{"class":228},[171,1325,380],{"class":224},[171,1327,383],{"class":228},[171,1329,1330],{"class":81,"line":221},[171,1331,236],{"emptyLinePlaceholder":235},[171,1333,1334,1337,1339,1342,1345,1347,1350,1352,1354,1356,1358,1360],{"class":81,"line":232},[171,1335,1336],{"class":228},"orders ",[171,1338,313],{"class":224},[171,1340,1341],{"class":228}," pd.read_excel(",[171,1343,1344],{"class":180},"\"orders.xlsx\"",[171,1346,589],{"class":228},[171,1348,1349],{"class":331},"dtype",[171,1351,313],{"class":224},[171,1353,1175],{"class":228},[171,1355,531],{"class":180},[171,1357,659],{"class":228},[171,1359,546],{"class":180},[171,1361,665],{"class":228},[171,1363,1364,1367,1369,1372],{"class":81,"line":239},[171,1365,1366],{"class":228},"adjustments ",[171,1368,313],{"class":224},[171,1370,1371],{"class":228}," df                                   ",[171,1373,1374],{"class":406},"# from the sheet, above\n",[171,1376,1377],{"class":81,"line":247},[171,1378,236],{"emptyLinePlaceholder":235},[171,1380,1381,1384,1386,1389,1392,1394,1396,1398,1401,1403,1406,1408,1411,1413,1415],{"class":81,"line":261},[171,1382,1383],{"class":228},"merged ",[171,1385,313],{"class":224},[171,1387,1388],{"class":228}," orders.merge(adjustments, ",[171,1390,1391],{"class":331},"on",[171,1393,313],{"class":224},[171,1395,531],{"class":180},[171,1397,589],{"class":228},[171,1399,1400],{"class":331},"how",[171,1402,313],{"class":224},[171,1404,1405],{"class":180},"\"left\"",[171,1407,589],{"class":228},[171,1409,1410],{"class":331},"indicator",[171,1412,313],{"class":224},[171,1414,950],{"class":269},[171,1416,343],{"class":228},[171,1418,1419,1422,1424,1427,1430,1433,1435,1438,1440],{"class":81,"line":266},[171,1420,1421],{"class":228},"unmatched ",[171,1423,313],{"class":224},[171,1425,1426],{"class":228}," adjustments[",[171,1428,1429],{"class":224},"~",[171,1431,1432],{"class":228},"adjustments[",[171,1434,531],{"class":180},[171,1436,1437],{"class":228},"].isin(orders[",[171,1439,531],{"class":180},[171,1441,1442],{"class":228},"])]\n",[171,1444,1445,1448,1450,1453,1455,1457,1460,1462,1464],{"class":81,"line":279},[171,1446,1447],{"class":228},"duplicated ",[171,1449,313],{"class":224},[171,1451,1452],{"class":228}," adjustments[adjustments.duplicated(",[171,1454,531],{"class":180},[171,1456,589],{"class":228},[171,1458,1459],{"class":331},"keep",[171,1461,313],{"class":224},[171,1463,618],{"class":269},[171,1465,1466],{"class":228},")]\n",[171,1468,1469],{"class":81,"line":288},[171,1470,236],{"emptyLinePlaceholder":235},[171,1472,1473,1475,1477,1479,1481,1483,1485,1488,1490,1493],{"class":81,"line":296},[171,1474,454],{"class":269},[171,1476,1281],{"class":228},[171,1478,1233],{"class":224},[171,1480,1246],{"class":180},[171,1482,1175],{"class":1174},[171,1484,711],{"class":269},[171,1486,1487],{"class":228},"(unmatched)",[171,1489,1181],{"class":1174},[171,1491,1492],{"class":180}," adjustment(s) match no order\"",[171,1494,343],{"class":228},[171,1496,1497,1499,1501,1503,1505,1507,1509,1512,1514,1517],{"class":81,"line":302},[171,1498,454],{"class":269},[171,1500,1281],{"class":228},[171,1502,1233],{"class":224},[171,1504,1246],{"class":180},[171,1506,1175],{"class":1174},[171,1508,711],{"class":269},[171,1510,1511],{"class":228},"(duplicated)",[171,1513,1181],{"class":1174},[171,1515,1516],{"class":180}," reference(s) adjusted more than once\"",[171,1518,343],{"class":228},[10,1520,1521,1522,27],{},"Writing those two frames back to the sheet as their own tabs closes the loop, because the people who created the discrepancies are the ones already looking at that document. The join techniques are covered in ",[23,1523,1525],{"href":1524},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffind-rows-in-one-excel-file-missing-from-another\u002F","Find rows in one Excel file missing from another",[157,1527,1529],{"id":1528},"schedule-the-sync","Schedule the sync",[10,1531,1532],{},"Nothing about this needs to be interactive. A cron entry, a systemd timer or a CI schedule runs it, provided the credentials reach the job:",[162,1534,1536],{"className":164,"code":1535,"language":166,"meta":167,"style":167},"GOOGLE_APPLICATION_CREDENTIALS=\u002Fetc\u002Fsecrets\u002Freporting-sa.json \\\n  \u002Fopt\u002Freporting\u002Fvenv\u002Fbin\u002Fpython \u002Fopt\u002Freporting\u002Fsync_sheets.py >> \u002Fvar\u002Flog\u002Fsync.log 2>&1\n",[14,1537,1538,1551],{"__ignoreMap":167},[171,1539,1540,1543,1545,1548],{"class":81,"line":173},[171,1541,1542],{"class":228},"GOOGLE_APPLICATION_CREDENTIALS",[171,1544,313],{"class":224},[171,1546,1547],{"class":180},"\u002Fetc\u002Fsecrets\u002Freporting-sa.json",[171,1549,1550],{"class":176}," \\\n",[171,1552,1553,1556,1559,1562,1565],{"class":81,"line":221},[171,1554,1555],{"class":180},"  \u002Fopt\u002Freporting\u002Fvenv\u002Fbin\u002Fpython",[171,1557,1558],{"class":180}," \u002Fopt\u002Freporting\u002Fsync_sheets.py",[171,1560,1561],{"class":224}," >>",[171,1563,1564],{"class":180}," \u002Fvar\u002Flog\u002Fsync.log",[171,1566,1567],{"class":224}," 2>&1\n",[10,1569,1570,1571,1575,1576,27],{},"Keep the key file readable only by the job's user, rotate it on the same schedule as any other credential, and never commit it. The scheduling patterns are in ",[23,1572,1574],{"href":1573},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron"," and ",[23,1577,1579],{"href":1578},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-python-excel-report-in-github-actions\u002F","Run a Python Excel report in GitHub Actions",[157,1581,1583],{"id":1582},"decide-which-side-owns-the-data","Decide which side owns the data",[10,1585,1586],{},"A two-way sync without an owner produces conflicts nobody can resolve: the sheet says one thing, the workbook another, and both were edited since the last run. Settle the question before writing any code.",[29,1588,38,1593,38,1596,38,1599,38,1602,38,1605,38,1609,38,1613,38,1617,38,1620,38,1624,38,1627,38,1630,38,1633,38,1636,38,1640,38,1644,38,1648,38,1651,38,1654],{"viewBox":1589,"role":32,"ariaLabelledBy":1590,"xmlns":36,"style":37},"0 0 760 210",[1591,1592],"gs3-t","gs3-d",[40,1594,1595],{"id":1591},"Three ownership models for a sheet-and-workbook pair",[44,1597,1598],{"id":1592},"Either the sheet is the source and the workbook is generated, or the system is the source and the sheet is a read-only view, or each side owns distinct columns.",[48,1600],{"x":50,"y":50,"width":51,"height":1601,"fill":53},"210",[55,1603,1604],{"x":57,"y":58,"style":59},"Pick one and enforce it in code",[48,1606],{"x":989,"y":1002,"width":1607,"height":1608,"rx":66,"fill":67,"stroke":68},"226","134",[55,1610,1612],{"x":1611,"y":64,"style":73},"141","sheet is the source",[55,1614,1616],{"x":1611,"y":72,"style":1615},"font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","people type there",[55,1618,1619],{"x":1611,"y":77,"style":1615},"workbook is generated",[55,1621,1623],{"x":1611,"y":1622,"style":78},"158","never write back",[48,1625],{"x":1626,"y":1002,"width":1607,"height":1608,"rx":66,"fill":113,"stroke":68},"268",[55,1628,1629],{"x":116,"y":64,"style":117},"system is the source",[55,1631,1632],{"x":116,"y":72,"style":1615},"sheet is a live view",[55,1634,1635],{"x":116,"y":77,"style":1615},"overwritten each run",[55,1637,1639],{"x":116,"y":1622,"style":1638},"font-size:11.5px;fill:var(--brand-strong,#4338ca);text-anchor:middle","protect it from edits",[48,1641],{"x":1642,"y":1002,"width":1643,"height":1608,"rx":66,"fill":143,"stroke":68},"508","224",[55,1645,1647],{"x":1646,"y":64,"style":147},"620","split by column",[55,1649,1650],{"x":1646,"y":72,"style":1615},"system writes facts",[55,1652,1653],{"x":1646,"y":77,"style":1615},"people write notes",[55,1655,1656],{"x":1646,"y":1622,"style":151},"update ranges, not tabs",[10,1658,1659],{},"The third model is the most common in practice and the one that needs the most care: update named ranges rather than clearing the tab, or a run will wipe the column of human notes it was never meant to touch. Protect the machine-owned columns in the sheet's own settings so an accidental edit is prevented rather than silently overwritten on the next run.",[157,1661,1663],{"id":1662},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1665,1666,1667,1675,1688,1699,1708],"ul",{},[1668,1669,1670,1674],"li",{},[1671,1672,1673],"strong",{},"Forgetting to share the document"," with the service account's email — the cause of nearly every 403.",[1668,1676,1677,1680,1681,1683,1684,1687],{},[1671,1678,1679],{},"Per-cell reads and writes."," Batch with ",[14,1682,462],{}," and a single ",[14,1685,1686],{},"update","; anything else exhausts the quota.",[1668,1689,1690,1695,1696,1698],{},[1671,1691,1692,1694],{},[14,1693,565],{}," in an update payload."," Replace with ",[14,1697,905],{}," before sending; the API rejects non-JSON values.",[1668,1700,1701,1704,1705,1707],{},[1671,1702,1703],{},"Duplicate headers in the sheet."," ",[14,1706,462],{}," raises on them, because the result is a dictionary per row.",[1668,1709,1710,1713],{},[1671,1711,1712],{},"Assuming formatting travels."," It does not; rebuild it on the Excel side.",[157,1715,1717],{"id":1716},"performance-and-scale-notes","Performance and scale notes",[10,1719,1720,1721,1725],{},"The API, not pandas, is the constraint. Google enforces per-minute read and write quotas, so a sync should make a small, fixed number of calls regardless of row count: one read per tab, one update per tab. Above a few tens of thousands of rows a spreadsheet is the wrong store entirely — move the data to a database and let Sheets hold only the manual layer, which is the pattern described in ",[23,1722,1724],{"href":1723},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fload-an-excel-file-into-a-sql-database-with-pandas\u002F","Load an Excel file into a SQL database with pandas",". Add retry with backoff on 429 responses so a busy minute delays the job rather than failing it.",[157,1727,1729],{"id":1728},"conclusion","Conclusion",[10,1731,1732,1733,1735,1736,1738,1739,1741],{},"A service account plus ",[14,1734,20],{}," turns Google Sheets into just another data source: authenticate once, read a tab in a single call, reshape in pandas, and write a formatted workbook. The reverse trip is one ",[14,1737,1686],{}," call, and the Drive export endpoint hands you the whole spreadsheet as ",[14,1740,16],{}," when no transformation is needed. Batch every access, replace missing values before sending, and keep the key file out of the repository — then schedule it and let the reconciliation run itself.",[157,1743,1745],{"id":1744},"frequently-asked-questions","Frequently asked questions",[10,1747,1748,1751],{},[1671,1749,1750],{},"Do I need OAuth or a service account?","\nA service account for anything unattended. It authenticates as itself with a JSON key, needs no browser consent, and you simply share the spreadsheet with its email address. OAuth is for tools acting on behalf of a signed-in person.",[10,1753,1754,1757,1758,1760],{},[1671,1755,1756],{},"Why does my script get a 403 on a sheet I can open?","\nYou are opening it as yourself; the script is a different identity. Share the spreadsheet with the service account's ",[14,1759,360],{},", exactly as you would with a colleague.",[10,1762,1763,1766],{},[1671,1764,1765],{},"How do I keep formatting when copying to Excel?","\nYou do not — gspread reads values. Read the values with gspread, then apply your own formatting with xlsxwriter or openpyxl on the way into the workbook.",[10,1768,1769,1772,1773,1775,1776,1778],{},[1671,1770,1771],{},"What are the API limits?","\nGoogle enforces per-minute read and write quotas per project and per user. Batch operations — one ",[14,1774,462],{}," instead of many cell reads, one ",[14,1777,1686],{}," instead of many — keep a sync comfortably inside them.",[10,1780,1781,1784],{},[1671,1782,1783],{},"Can I just download the spreadsheet as xlsx?","\nYes. The Drive export endpoint returns the whole spreadsheet as a workbook, which is the simplest route when you want every tab and do not need to transform anything.",[157,1786,1788],{"id":1787},"related","Related",[1665,1790,1791,1799,1806,1811,1816],{},[1668,1792,1793,1704,1796,1798],{},[1671,1794,1795],{},"Up:",[23,1797,26],{"href":25}," — the other systems a reporting job connects to.",[1668,1800,1801,1805],{},[23,1802,1804],{"href":1803},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Ffetch-api-data-into-excel-with-python-requests\u002F","Fetch API data into Excel with Python requests"," — the same shape of job against a generic HTTP API.",[1668,1807,1808,1810],{},[23,1809,1724],{"href":1723}," — where the data should live once a spreadsheet outgrows itself.",[1668,1812,1813,1815],{},[23,1814,1525],{"href":1524}," — the reconciliation this sync exists to support.",[1668,1817,1818,1822],{},[23,1819,1821],{"href":1820},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fsave-excel-reports-to-google-drive-with-python\u002F","Save Excel reports to Google Drive with Python"," — publishing the finished workbook back to the same account.",[1824,1825,1826],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":167,"searchDepth":221,"depth":221,"links":1828},[1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842],{"id":159,"depth":221,"text":160},{"id":202,"depth":221,"text":203},{"id":364,"depth":221,"text":365},{"id":569,"depth":221,"text":570},{"id":733,"depth":221,"text":734},{"id":1046,"depth":221,"text":1047},{"id":1308,"depth":221,"text":1309},{"id":1528,"depth":221,"text":1529},{"id":1582,"depth":221,"text":1583},{"id":1662,"depth":221,"text":1663},{"id":1716,"depth":221,"text":1717},{"id":1728,"depth":221,"text":1729},{"id":1744,"depth":221,"text":1745},{"id":1787,"depth":221,"text":1788},"2026-08-27","Move data both ways between Google Sheets and .xlsx: service-account auth with gspread, reading a tab into pandas, writing back, exporting a whole spreadsheet and scheduling it.","md",[1847,1849,1851,1853,1855],{"q":1750,"a":1848},"A service account for anything unattended. It authenticates as itself with a JSON key, needs no browser consent, and you simply share the spreadsheet with its email address. OAuth is for tools acting on behalf of a signed-in person.",{"q":1756,"a":1850},"You are opening it as yourself; the script is a different identity. Share the spreadsheet with the service account's client_email, exactly as you would with a colleague.",{"q":1765,"a":1852},"You do not — gspread reads values. Read the values with gspread, then apply your own formatting with xlsxwriter or openpyxl on the way into the workbook.",{"q":1771,"a":1854},"Google enforces per-minute read and write quotas per project and per user. Batch operations — one get_all_records instead of many cell reads, one update instead of many — keep a sync comfortably inside them.",{"q":1783,"a":1856},"Yes. The Drive export endpoint returns the whole spreadsheet as a workbook, which is the simplest route when you want every tab and do not need to transform anything.",{},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fsync-google-sheets-with-excel-using-python",{"title":1860,"description":1861},"Sync Google Sheets and Excel in Python","Read and write Google Sheets from Python with gspread and a service account, convert tabs to xlsx, push an Excel report back to Sheets, and run the sync on a schedule.","sync-google-sheets-with-excel-using-python","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fsync-google-sheets-with-excel-using-python\u002Findex","how-to","UiM3H158aouchZltjg1Jww2SMjQ9KthWLDHY8QqgGU0",[1867,1871],{"title":1868,"path":1869,"stem":1870,"children":-1},"Refresh an Excel Report from a Database on a Schedule","\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule","advanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule\u002Findex",{"title":1872,"path":1873,"stem":1874,"children":-1},"Reading Excel with Polars and Arrow","\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow","advanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Findex",1788710155288]