danthelion: @burnash What do you think about this implementation? Details are in the referenced issue.
burnash: @danthelion thank you for the issue and the PR.

As I said in the referenced issue I agree. I'm sorry for the delay in responding. Since you submitted the PR the code of gspread has significantly changed. If you have time, could you rebase your changes to resolve the conflicts? Also please don't forget to add a test to cover this fix.
danthelion: Sure, I'll get on it as soon as I can. 👍 
diff --git a/gspread/models.py b/gspread/models.py
index db592c7..30b36ea 100644
--- a/gspread/models.py
+++ b/gspread/models.py
@@ -545,11 +545,11 @@ class Worksheet(object):
         except KeyError:
             return []
 
-    def get_all_records(self, empty2zero=False, head=1, default_blank=""):
-        """Returns a list of dictionaries, all of them having the contents
-        of the spreadsheet with the head row as keys and each of these
-        dictionaries holding the contents of subsequent rows of cells
-        as values.
+    def get_all_records(self, empty2zero=False, head=1, default_blank="", allow_underscores_in_numeric_literals=False):
+        """Returns a list of dictionaries, all of them having:
+            - the contents of the spreadsheet's with the head row as keys,
+            And each of these dictionaries holding
+            - the contents of subsequent rows of cells as values.
 
         Cell values are numericised (strings that can be read as ints
         or floats are converted).
@@ -564,13 +564,17 @@ class Worksheet(object):
                               converted to something else except empty string
                               or zero.
         :type default_blank: str
+        :param allow_underscores_in_numeric_literals: (optional) Allow underscores 
+                                                      in numeric literals,
+                                                      as introduced in PEP 515
+        :type allow_underscores_in_numeric_literals: bool
         """
 
         idx = head - 1
 
         data = self.get_all_values()
         keys = data[idx]
-        values = [numericise_all(row, empty2zero, default_blank)
+        values = [numericise_all(row, empty2zero, default_blank, allow_underscores_in_numeric_literals)
                   for row in data[idx + 1:]]
 
         return [dict(zip(keys, row)) for row in values]
diff --git a/gspread/utils.py b/gspread/utils.py
index 0854611..a966820 100644
--- a/gspread/utils.py
+++ b/gspread/utils.py
@@ -29,7 +29,7 @@ def finditem(func, seq):
     return next((item for item in seq if func(item)))
 
 
-def numericise(value, empty2zero=False, default_blank=""):
+def numericise(value, empty2zero=False, default_blank="", allow_underscores_in_numeric_literals=False):
     """Returns a value that depends on the input string:
         - Float if input can be converted to Float
         - Integer if input can be converted to integer
@@ -42,6 +42,10 @@ def numericise(value, empty2zero=False, default_blank=""):
     'faa'
     >>> numericise("3")
     3
+    >>> numericise("3_2", allow_underscores_in_numeric_literals=False)
+    '3_2'
+    >>> numericise("3_2", allow_underscores_in_numeric_literals=True)
+    '32'
     >>> numericise("3.1")
     3.1
     >>> numericise("", empty2zero=True)
@@ -58,6 +62,8 @@ def numericise(value, empty2zero=False, default_blank=""):
     >>>
     """
     if value is not None:
+        if "_" in value and not allow_underscores_in_numeric_literals:
+            return value
         try:
             value = int(value)
         except ValueError:
@@ -73,9 +79,9 @@ def numericise(value, empty2zero=False, default_blank=""):
     return value
 
 
-def numericise_all(input, empty2zero=False, default_blank=""):
+def numericise_all(input, empty2zero=False, default_blank="", allow_underscores_in_numeric_literals=False):
     """Returns a list of numericised values from strings"""
-    return [numericise(s, empty2zero, default_blank) for s in input]
+    return [numericise(s, empty2zero, default_blank, allow_underscores_in_numeric_literals) for s in input]
 
 
 def rowcol_to_a1(row, col):

Ambiguity in the `numericise` function depending on Python version.
https://github.com/burnash/gspread/blob/217b43073e1abe9ceb1d65d2d0719d56f0a14642/gspread/utils.py#L58

I have run into some issues when fetching data from spreadsheets using the `get_all_records()` method when using Python 3.6+.

If a worksheet has data in the format `12_34`, which should be interpreted as a string (or numeric if _explicitly_ desired) the `numericise` function will convert it to an `int` because of https://docs.python.org/3/whatsnew/3.6.html#whatsnew36-pep515 introduced in Python3.6, using Python3.5.4 it parses it as the string `12_34`, as expected.

I don't think following PEP515 should be the default behaviour in this case.

Example

Python3.6
```
>>> from gspread.utils import numericise
>>> numericise('18_29')
1829
```

Python3.5.4
```
>>> from gspread.utils import numericise
>>> numericise('18_29')
'18_29'
```
diff --git a/tests/test.py b/tests/test.py
index 431c0f7..ee42246 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -163,6 +163,20 @@ class UtilsTest(unittest.TestCase):
         gid = 'ogsrar0'
         self.assertEqual(utils.wid_to_gid(gid), '1015761654')
 
+    def test_numericise(self):
+        self.assertEqual(utils.numericise('faa'), 'faa')
+        self.assertEqual(utils.numericise('3'), 3)
+        self.assertEqual(utils.numericise('3_2'), '3_2')
+        self.assertEqual(utils.numericise('3_2', allow_underscores_in_numeric_literals=False), '3_2')
+        self.assertEqual(utils.numericise('3_2', allow_underscores_in_numeric_literals=True), 32)
+        self.assertEqual(utils.numericise('3.1'), 3.1)
+        self.assertEqual(utils.numericise('', empty2zero=True), 0)
+        self.assertEqual(utils.numericise('', empty2zero=False), '')
+        self.assertEqual(utils.numericise('', default_blank=None), None)
+        self.assertEqual(utils.numericise('', default_blank='foo'), 'foo')
+        self.assertEqual(utils.numericise(''), '')
+        self.assertEqual(utils.numericise(None), None)
+
 
 class GspreadTest(BetamaxGspreadTest):
     def _sequence_generator(self):

pip install -e .[dev]
parse_log_pytest
requirements.txt
nose requests[security] oauth2client pytest
3.9
test-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
betamax==0.8.1
-e git+https://github.com/burnash/betamax-json-body-serializer.git@0945268b69272cf90c55fdfd962f1801295ff30b#egg=betamax_json_body_serializer
certifi==2025.1.31
charset-normalizer==3.4.1
exceptiongroup==1.2.2
-e git+https://github.com/burnash/gspread.git@c65b3893d02d8e3896cfb7dd2d20eb2f0bdcdeb4#egg=gspread
httplib2==0.22.0
idna==3.10
iniconfig==2.1.0
nose==1.3.7
oauth2client==4.1.3
packaging==24.2
pluggy==1.5.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
pyparsing==3.2.3
pytest==8.3.5
requests==2.32.3
rsa==4.9
six==1.17.0
tomli==2.2.1
urllib3==2.3.0

name: gspread
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - betamax==0.8.1
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - exceptiongroup==1.2.2
      - httplib2==0.22.0
      - idna==3.10
      - iniconfig==2.1.0
      - nose==1.3.7
      - oauth2client==4.1.3
      - packaging==24.2
      - pluggy==1.5.0
      - pyasn1==0.6.1
      - pyasn1-modules==0.4.2
      - pyparsing==3.2.3
      - pytest==8.3.5
      - requests==2.32.3
      - rsa==4.9
      - six==1.17.0
      - tomli==2.2.1
      - urllib3==2.3.0
prefix: /opt/conda/envs/gspread

tests/test.py::UtilsTest::test_numericise
tests/test.py::UtilsTest::test_a1_to_rowcol tests/test.py::UtilsTest::test_addr_converters tests/test.py::UtilsTest::test_extract_id_from_url tests/test.py::UtilsTest::test_get_gid tests/test.py::UtilsTest::test_no_extract_id_from_url tests/test.py::UtilsTest::test_rowcol_to_a1 tests/test.py::ClientTest::test_create tests/test.py::ClientTest::test_import_csv tests/test.py::ClientTest::test_no_found_exeption tests/test.py::ClientTest::test_openall tests/test.py::SpreadsheetTest::test_add_del_worksheet tests/test.py::SpreadsheetTest::test_get_worksheet tests/test.py::SpreadsheetTest::test_properties tests/test.py::SpreadsheetTest::test_sheet1 tests/test.py::SpreadsheetTest::test_worksheet tests/test.py::SpreadsheetTest::test_worksheet_iteration tests/test.py::WorksheetTest::test_acell tests/test.py::WorksheetTest::test_append_row tests/test.py::WorksheetTest::test_cell tests/test.py::WorksheetTest::test_clear tests/test.py::WorksheetTest::test_delete_row tests/test.py::WorksheetTest::test_find tests/test.py::WorksheetTest::test_findall tests/test.py::WorksheetTest::test_get_all_records tests/test.py::WorksheetTest::test_get_all_records_different_header tests/test.py::WorksheetTest::test_get_all_values tests/test.py::WorksheetTest::test_insert_row tests/test.py::WorksheetTest::test_range tests/test.py::WorksheetTest::test_resize tests/test.py::WorksheetTest::test_update_acell tests/test.py::WorksheetTest::test_update_cell tests/test.py::WorksheetTest::test_update_cell_multiline tests/test.py::WorksheetTest::test_update_cell_unicode tests/test.py::WorksheetTest::test_update_cells tests/test.py::WorksheetTest::test_update_cells_noncontiguous tests/test.py::WorksheetTest::test_update_cells_unicode tests/test.py::CellTest::test_numeric_value tests/test.py::CellTest::test_properties
gspread/models.py gspread/utils.py
gspread/models.py gspread/utils.py